/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

/*
 * Created on 12/07/2004
 *
 */

package au.com.lastweekend.jim.io.jpeg;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.io.jpeg.write.JpegTransform;
import au.com.lastweekend.jim.io.jpeg.write.JpegTransform.Coordinate;

/**
 * Represents a multi-component scan of jpeg data
 * 
 * @version $Id: Scan.java,v 1.3 2006/01/28 05:05:40 ggardner Exp $
 * @author grant@lastweekend.com.au
 * 
 */
public class Scan {

    private static final Logger LOG = Logger.getLogger(Scan.class);
    private static final int MAX_COMPONENTS = 4;
    private static final int MAX_DECODERS = 4;
    // Allow at most 4 decoders for each of DC/AC co-efficients
    private HuffmanTable[] huffmanACDecoders = new HuffmanTable[MAX_DECODERS];
    private HuffmanTable[] huffmanDCDecoders = new HuffmanTable[MAX_DECODERS];

    // Allow at most 4 components.
    private ScanComponent[] scanComponents = new ScanComponent[MAX_COMPONENTS];
    private int width;
    private int height;
    private int mcuRows;
    private int mcuColumns;
    private int restartInterval = -1;

    /**
     * @param isACTable
     * @param tableIndex
     * @param decoder
     */
    public void setHuffmanDecoder(boolean isACTable, byte tableIndex, HuffmanTable decoder) {

        int index = tableIndex;
        if (isACTable) {
            huffmanACDecoders[index] = decoder;
        } else {
            huffmanDCDecoders[index] = decoder;

        }

    }

    /**
     * @param componentID
     * @param horizontalSamplingFactor
     * @param verticalSamplingFactor
     */
    public void createComponent(byte componentID, byte horizontalSamplingFactor, byte verticalSamplingFactor) {

        ScanComponent comp = new ScanComponent(this, componentID, horizontalSamplingFactor, verticalSamplingFactor);
        scanComponents[comp.getComponentID()] = comp;

    }

    /**
     * @param componentID
     * @param dcDecoderIndex
     * @param acDecoderIndex
     */
    public void setComponentDecoders(byte componentID, byte dcDecoderIndex, byte acDecoderIndex) {

        int componentIndex = componentID;
        ScanComponent scanComponent = scanComponents[componentIndex];
        scanComponent.setDCDecoder(huffmanDCDecoders[dcDecoderIndex]);
        scanComponent.setACDecoder(huffmanACDecoders[acDecoderIndex]);

    }

    /**
     * @param inStream
     * @throws JpegProcessingException
     */
    public void read(InputStream inStream) throws JpegProcessingException {

        initialiseScan();

        // JpegBitBuffer, parses the bit stream doing the byte stuffing thing
        // for 0xFF bytes.
        JpegBitBuffer bitBuffer = new JpegBitBuffer(inStream);
        int mcusToRestart = restartInterval;
        for (int mcuY = 0; mcuY < mcuRows; mcuY++) {
            for (int mcuX = 0; mcuX < mcuColumns; mcuX++) {
                if (mcusToRestart == 0) {
                    restartDecoder(inStream);
                    bitBuffer = new JpegBitBuffer(inStream);
                    mcusToRestart = restartInterval;
                }

                decodeMCU(mcuX, mcuY, bitBuffer);
                mcusToRestart--;

            }
        }

    }

    /**
     * @param inStream
     */
    private void restartDecoder(InputStream inStream) throws JpegProcessingException {

        if (LOG.isDebugEnabled()) {
            LOG.debug("Processing Restart marker");
        }
        try {
            byte markerBoundary = (byte) inStream.read();
            byte restartMarker = (byte) inStream.read();
            if (LOG.isDebugEnabled()) {
                LOG.debug("Markers boundary=" + markerBoundary + " restart=" + restartMarker);

            }
        } catch (IOException e) {
            throw new JpegProcessingException(e);
        }
        for (int c = 0; c < MAX_COMPONENTS; c++) {
            ScanComponent scanComponent = scanComponents[c];
            if (scanComponent != null) {
                scanComponent.restartDecoder();
            }
        }
    }

    private void decodeMCU(int mcuX, int mcuY, JpegBitBuffer bitBuffer) throws JpegProcessingException {

        // if (LOG.isDebugEnabled()) {
        // LOG.debug("Reading MCU x=" + mcuX + " y=" + mcuY);
        // }
        /*
         * The spec may in fact insist that the components are numbered 1,2,3, such that you can't have 1,3,4 which would mean that
         * for grayscale or YCbCr images, we could skip a couple of thousand ==null checks.
         */

        for (int c = 0; c < MAX_COMPONENTS; c++) {
            ScanComponent scanComponent = scanComponents[c];
            if (scanComponent != null) {
                scanComponent.readMCU(mcuX, mcuY, bitBuffer);
            }
        }
    }

    /**
     * @param numberOfMCUs
     */
    private void initialiseScan() {

        /*
         * Jpeg spec A.1.1 Because I didn't conclude this immediately, the derivation of number of MCU's is as follows...
         * 
         * Xi = number of horizonal samples for component i Xi = X * Hi/HMax;
         * 
         * Xi/Hi = X / HMax;
         * 
         * and similarly for vertical samples
         * 
         * and then we are representing 8x8 blocks of samples...
         * 
         * and we need to extend it to 8x8
         */

        int maxHorizontalSamplingFactor = 0;
        int maxVerticalSamplingFactor = 0;
        for (int i = 0; i < MAX_COMPONENTS; i++) {
            ScanComponent scanComponent = scanComponents[i];
            if (scanComponent != null) {
                maxHorizontalSamplingFactor = Math.max(maxHorizontalSamplingFactor, scanComponent.getHorizontalSamplingFactor());
                maxVerticalSamplingFactor = Math.max(maxVerticalSamplingFactor, scanComponent.getVerticalSamplingFactor());

            }
        }

        mcuRows = divCeil(divCeil(height, maxVerticalSamplingFactor), 8);
        mcuColumns = divCeil(divCeil(width, maxHorizontalSamplingFactor), 8);
        LOG.debug("mcuRows=" + mcuRows + " mcuColumns=" + mcuColumns);
        for (int c = 0; c < MAX_COMPONENTS; c++) {
            ScanComponent scanComponent = scanComponents[c];
            if (scanComponent != null) {
                scanComponent.initialiseScan(mcuColumns, mcuRows);
            }
        }
    }

    private int divCeil(int dividend, int divisor) {

        int result = dividend / divisor;
        if (dividend % divisor != 0) {
            result++;
        }
        return result;
    }

    /**
     * @param frameHeight
     */
    public void setHeight(int frameHeight) {

        height = frameHeight;

    }

    /**
     * @param frameWidth
     */
    public void setWidth(int frameWidth) {

        width = frameWidth;

    }

    /**
     * @param transform
     * @param outStream
     * @throws JpegProcessingException
     */
    public void writeTransform(JpegTransform transform, OutputStream outStream) throws JpegProcessingException {

        JpegBitOutput bitOutput = new JpegBitOutput(outStream);

        initialiseTransform(transform);

        Coordinate transformedDimensions = transform.transform(getMCUColumns(), getMCURows());
        int mcuRows = transformedDimensions.getHeight();
        int mcuColumns = transformedDimensions.getWidth();

        int mcusToRestart = restartInterval;
        int restarts = 0;
        for (int mcuY = 0; mcuY < mcuRows; mcuY++) {
            for (int mcuX = 0; mcuX < mcuColumns; mcuX++) {

                if (mcusToRestart == 0) {
                    bitOutput.alignBytes();
                    restartEncoder(outStream, restarts++);
                    mcusToRestart = restartInterval;
                }

                if (bitOutput.isDebugEnabled()) {
                    bitOutput.debug("Writing MCU x=" + mcuX + " y=" + mcuY);
                }
                for (int c = 0; c < MAX_COMPONENTS; c++) {
                    ScanComponent scanComponent = scanComponents[c];
                    if (scanComponent != null) {
                        scanComponent.writeMCU(mcuX, mcuY, bitOutput);

                    }
                }

                mcusToRestart--;

            }
        }

        bitOutput.alignBytes();

    }

    /**
     * @param outStream
     * @param restarts
     * @throws JpegProcessingException
     * 
     */
    private void restartEncoder(OutputStream outStream, int restarts) throws JpegProcessingException {

        try {
            outStream.write(JpegSegment.SEGMENT_BOUNDARY);
            outStream.write(JpegSegment.RESTARTS_BEGIN + (restarts % 8));

            for (int c = 0; c < MAX_COMPONENTS; c++) {
                ScanComponent scanComponent = scanComponents[c];
                if (scanComponent != null) {
                    scanComponent.restartEncoder();
                }
            }

        } catch (IOException e) {
            throw new JpegProcessingException(e);
        }

    }

    /**
     * @param transform
     */
    private void initialiseTransform(JpegTransform transform) {

        for (int c = 0; c < MAX_COMPONENTS; c++) {
            ScanComponent scanComponent = scanComponents[c];
            if (scanComponent != null) {
                scanComponent.initialiseTransform(transform);
            }
        }
    }

    public int getMCURows() {

        return mcuRows;
    }

    public int getMCUColumns() {

        return mcuColumns;
    }

    /**
     * @param restartInterval
     */
    public void setRestartInterval(int restartInterval) {

        this.restartInterval = restartInterval;

    }

}